![]() |
![]() |
|
Diese Schnittstellen bilden eine Hierarchie, deren Wurzel IEnumerable ist, aus der ICollection abgeleitet wird. Beide Schnittstellen sind charakteristisch für Auflistungsklassen, denn sie stellen die wichtigsten Grundfunktionalitäten bereit. IDictionary und IList leiten sich zudem aus ICollection ab und spalten die Auflistungsklassen in zwei Gruppen: 1. Klassen, die das Interface IList implementieren, beschreiben Objektauflistungen, auf deren Einträge über einen Index zugegriffen wird.
Abbildung 7.1 Die Schnittstellen der Auflistungsklassen Die Schnittstelle »IEnumerable«Diese Schnittstelle hat nur die Methode GetEnumerator, die ein Enumerator-Objekt bereitstellt. Ein Enumerator verfügt über die Fähigkeit, eine Auflistung elementweise zu durchlaufen. Damit gleicht dieses Objekt einem Positionszeiger, dem drei Methoden eigen sind: Current, MoveNext und Reset. Der Enumerator positioniert sich standardmäßig vor dem ersten Eintrag einer Auflistung. Um ihn auf den ersten Eintrag und anschließend auf alle Folgeeinträge zeigen zu lassen, muss die Methode MoveNext ausgeführt werden. Mit Current wird auf den Eintrag zugegriffen, auf den der Enumerator aktuell zeigt. Reset setzt den Enumerator in seine Ausgangsposition zurück, also vor den ersten Eintrag. Es gibt eine Situation, in der die Fähigkeit des Enumerators ausgesprochen wichtig ist. Es ist die foreach-Schleife, mit der eine Auflistung vom ersten bis zum letzten Element durchlaufen wird:
Die Schnittstelle »ICollection«Die Schnittstelle ICollection stattet alle Auflistungen mit weiteren Fähigkeiten aus. Diese Schnittstelle veröffentlicht die in der folgenden Tabelle aufgeführten Eigenschaften und Methoden.
Insbesondere Auflistungen sind kritisch hinsichtlich des gleichzeitigen Zugriffs mehrerer Threads. Viele Klassen implementieren Thread-Sicherheit durch die Bereitstellung der Methode Synchronized, beispielsweise auch die Klassen ArrayList und Hashtable. Die Eigenschaft IsSynchronized gibt an, ob die Auflistung synchronisiert ist. Weitere Informationen zu Threads, der Synchronisierung und der Eigenschaft SyncRoot erhalten Sie in Kapitel 11. Die Schnittstelle »IList«Auflistungen, die IList implementieren, zeichnen sich dadurch aus, ihre Elemente über Indizes verwalten zu können. Das beste Beispiel hierfür dürfte die Klasse ArrayList sein, aber auch eine große Anzahl weiterer, meist steuerelementspezifischer Auflistungen gehört zu dieser Gruppe. Wir wollen uns daher zuerst die wichtigsten Eigenschaften und Methoden ansehen, die alle Klassen, die IList implementieren, gemeinsam aufweisen.
Ihre Stärke spielen Schnittstellen aus, wenn sie von mehreren Klassen implementiert werden. Jede Klasse weist dann dieselben Merkmale und Verhaltensweisen auf. Wenn Sie mit einer Klasse gearbeitet haben, die eine oder mehrere gängige Schnittstellen unterstützt, sollten Sie auch mit allen anderen Klassen umgehen können, welche die gleichen Schnittstellen unterstützen. Das trifft insbesondere auf die Schnittstelle IList zu, weil sie von sehr vielen Klassen des .NET Frameworks implementiert wird. Es ist daher empfehlenswert, sich insbesondere mit den Eigenschaften und Methoden dieser Schnittstelle vertraut zu machen. Beispiele dazu werden Ihnen im weiteren Verlauf dieses Buchs noch ausgesprochen viele begegnen. 7.3.2 Die Klasse »ArrayList«
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ArrayList arr = new ArrayList(); |
| int[] intArr = {0, 10, 22, 9, 45}; |
| arr.AddRange(intArr); |
Liegt das Array bereits vor der Instanziierung von ArrayList vor, kann das Array auch direkt dem Konstruktor übergeben werden:
| ArrayList arr = new ArrayList(intArr); |
Nehmen wir an, mehrere Objekte vom Typ ClassA sollen von einer ArrayList verwaltet werden. ClassA sei wie folgt definiert:
| public class ClassA { |
| public int Prop; |
| public ClassA(int x) { |
| Prop = x; |
| } |
Im folgenden Code werden die beiden Referenzen obj1 und obj2 vom Typ ClassA der Auflistung col hinzugefügt. Die Auflistung col verwaltet danach obj1 über den Index 0 und obj2 über den Index 1.
| ClassA obj1 = new ClassA(1); |
| ClassA obj2 = new ClassA(2); |
| // Klasse ArrayList instanziieren |
| ArrayList col = new ArrayList(); |
| // obj1 und obj2 der Auflistung hinzufügen |
| col.Add(obj1); |
| col.Add(obj2); |
Damit hätten wir aktuell die folgenden Zuordnungen:
| Der Listeneintrag col[0] enthält die Referenz obj1. |
| Der Listeneintrag col[1] enthält die Referenz obj2. |
Mit Add haben Sie keinen Einfluss auf die Positionierung der Objekte innerhalb der Liste. Wollen Sie ein Objekt jedoch an einer bestimmten Position einsortieren, sollten Sie anstelle der Add-Methode die Methode Insert benutzen:
| ClassA obj3 = new ClassA(3); |
| col.Insert(1, obj3); |
Das über obj3 referenzierte Objekt wird damit unter dem Index 1 registriert. Falls man versucht, einen Index anzugeben, der größer ist als die Anzahl der Elemente in der Auflistung, kommt es zur Ausnahme ArgumentOutOfRangeException, da der letzte verfügbare Index immer genauso groß ist, wie die Auflistung Elemente enthält.
Was passiert aber mit dem Element obj2, das vorher die Position des Index 1 einnahm? Wir können das prüfen, indem wir beispielsweise eine Methode schreiben, der wir die Referenz auf die Auflistung als Argument übergeben. In der Methode werden die Elemente der übergebenen Auflistung vom ersten bis zum letzten Objekt an der Konsole ausgegeben:
| // Auflistung enthält nur typgleiche Einträge |
| public void GetListElements(IList list) { |
| foreach(ClassA temp in list) { |
| Console.Write("Collection-Index = {0}", list.IndexOf(temp)); |
| Console.WriteLine(" / Objekt-Nr.{0}", temp.intProp); |
| } |
| } |
Der Typ des Parameters ist IList. Wir hätten auch den Typ ArrayList wählen können, halten uns aber mit unserer Festlegung allgemeiner und haben damit eine Methode, die jeden beliebigen Auflistungstyp entgegennimmt, der IList implementiert und ClassA-Objekte verwaltet.
In der Methode GetListElements wird in einer foreach-Schleife die Auflistung vom ersten bis zum letzten Element durchlaufen. Die Laufvariable temp ist vom Typ ClassA deklariert, da wir wissen, dass unsere Auflistung nur Objekte dieses Typs enthält.
Typgleiche Objekte in einer der Auflistungsklassen zu verwalten, ist die Regel. Der Grund dafür ist einleuchtend, denn innerhalb der Schleife wird häufig ein typspezifisches Member des sich aktuell im Zugriff befindlichen Objekts aufgerufen. Verwaltet eine Auflistung unterschiedliche Typen, muss die Laufvariable der Schleife allgemeiner typisiert werden. Innerhalb des Schleifenblocks sind dann eine Typüberprüfung sowie eine Typumwandlung erforderlich, um auf ein spezifisches Merkmal des Objekts zuzugreifen. Das geht natürlich zu Lasten der Performance.
| // Auflistung enthält unterschiedliche Typen |
| public static void GetListElements(IList list) { |
| foreach(object temp in list) { |
| if(temp is ClassA) { |
| Console.Write("Collection-Index = {0}", list.IndexOf(temp)); |
| Console.WriteLine(" / Objekt-Nr.{0}", ((ClassA)temp).intProp); |
| } |
| } |
| } |
Sehen wir uns nun die Ausgabe an, die von der Prozedur GetListElements in das Konsolenfenster geschrieben wird, nachdem wir mit Insert ein drittes Objekt an die zweite Listenposition gesetzt haben:
| Collection-Index = 0 / Objekt-Nr.1 |
| Collection-Index = 1 / Objekt-Nr.3 |
| Collection-Index = 2 / Objekt-Nr.2 |
Das ursprünglich dem Index 1 zugeordnete Objekt musste seine Position räumen – es verschiebt sich in der Liste um eine Position in Richtung Listenende, während sich obj3 wunschgemäß einreiht. Hätten wir noch weitere Elemente in unserer Auflistung, würde sich die Indizierung aller Folgeelemente ebenfalls um eine Position verschieben.
Ein ähnliches Verhalten zeigt sich auch, wenn wir mit Remove oder RemoveAt aus der Auflistung einen Eintrag löschen: Der freigegebene Index bleibt nicht unbelegt, sondern bewirkt eine Indexverschiebung aller Nachfolgeelemente. Löschen wir beispielsweise das Element mit dem Index 2, rutscht das Objekt mit dem ursprünglichen Index 3 an die frei gewordenen Position 2, das Objekt mit dem Index 4 füllt die Lücke des Index 3 usw. (siehe auch Abbildung 7.2).
Die Tragweite dieser Elementverwaltung ist weitreichend, denn es kann zu keinem Zeitpunkt die eindeutige Zuordnung eines Objekts zu einem bestimmten Index in der Auflistung garantiert werden.
Auf der Buch-CD finden Sie den Programmcode des Beispiels unter:
...\Kapitel 7\ArrayListDemo

Hier klicken, um das Bild zu vergrößern
Abbildung 7.2 Listenverwaltung beim Löschen
Auflistungen zeichnen sich durch die beiden Interfaces IEnumerable und ICollection aus. Aus der letztgenannten stammt die Methode CopyTo, die es ermöglicht, die Einträge einer Auflistung in ein Array zu kopieren.
| ArrayList col = new ArrayList(); |
| col.Add("Anton"); |
| col.Add("Gustaf"); |
| col.Add("Fritz"); |
| string[] strArr = new string[10]; |
| col.CopyTo(strArr, 3); |
Der zweite Parameter von CopyTo gibt den Startindex im Array an, ab dem kopiert wird. Das Array muss groß genug sein, um alle Elemente aufzunehmen, sonst wird ein Fehler ausgelöst. Handelt es sich bei den zu kopierenden Einträgen um Objektreferenzen, werden nicht die Objekte, sondern nur die Referenzen kopiert. ArrayList überlädt CopyTo, so dass auch spezifizierte Teilbereiche der Liste kopiert werden können.
Die von ArrayList verwalteten Objekte sind sortierbar. Das ist keineswegs eine Selbstverständlichkeit, sondern ein wichtiges Charakteristikum dieser Klasse, wie wir später noch im Vergleich mit anderen Auflistungen feststellen werden.
Um die Mitglieder zu sortieren, wird die Methode Sort auf der ArrayList-Referenz aufgerufen. Sort ist mehrfach überladen. Wir wollen uns zunächst mit der parameterlosen Version beschäftigen:
| public virtual void Sort(); |
Die Regel, nach der im deutsprachigen Raum sortiert wird, vergleicht die Zeichen unter Berücksichtigung der Groß- und Kleinschreibung wie folgt:
| 1 < 2 ... < a < A < b < B < c < C ... < y < Y < z < Z |
Um die verwalteten Objekte einer ArrayList mit der parameterlosen Sort-Methode zu sortieren, müssen die Objekte die Schnittstelle IComparable implementieren. Diese Schnittstelle enthält nur die Methode CompareTo:
| public interface IComparable { |
| int CompareTo(object obj); |
| } |
Eine Klasse, die IComparable implementiert, garantiert, die Methode CompareTo zu veröffentlichen. Darauf ist die Sort-Methode der ArrayList angewiesen. Was eine Schnittstellenmethode leisten muss, ist der jeweiligen Dokumentation zu entnehmen. Aus der .NET-Dokumentation zu CompareTo können wir entnehmen, dass das aktuelle Objekt mit dem des Parameters verglichen wird. Als Resultat liefert der Methodenaufruf einen der drei folgenden Werte:
| < 0, wenn das aktuelle Objekt »kleiner« als das Objekt obj ist. |
| 0, wenn das aktuelle Objekt »gleich« dem Objekt obj ist. |
| > 0, wenn das aktuelle Objekt »größer« als das Objekt obj ist. |
Die Kriterien, was im Vergleich als »kleiner«, »gleich« und »größer« bewertet wird, muss die Klasse festlegen, welche die Schnittstelle IComparable implementiert. Sehen wir uns dazu ein Beispiel an:
| public class HoldValue : IComparable { |
| public int intVar; |
| public HoldValue(int x) { |
| intVar = x; |
| } |
| public int CompareTo(object obj) { |
| HoldValue val = (HoldValue)obj; |
| if(val.intVar < this.intVar) |
| return 1; |
| else if(val.intVar == this.intVar) |
| return 0; |
| else |
| return –1; |
| } |
| } |
Die Klasse HoldValue implementiert IComparable. Daher sind Objekte dieses Typs darauf vorbereitet, in einer ArrayList sortiert zu werden. Die Sortierreihenfolge soll sich am Inhalt des Felds intVar orientieren. In der Methode CompareTo wird die dem Parameter übergebene Referenz zuerst in den Typ HoldValue konvertiert und einer lokalen Variablen zugewiesen. Anschließend folgt ein Vergleich zwischen den Feldwerten des aktuellen und des übergebenen Objekts.
Natürlich wollen wir nun auch testen, ob wir unser Ziel erreicht haben. Dazu dient der folgende Testcode:
| static void Main(string[] args) { |
| ArrayList arrList = new ArrayList(); |
| HoldValue obj1 = new HoldValue(17); |
| arrList.Add(obj1); |
| HoldValue obj2 = new HoldValue(110); |
| arrList.Add(obj2); |
| HoldValue obj3 = new HoldValue(5); |
| arrList.Add(obj3); |
| arrList.Sort(); |
| int i = 0; |
| foreach(HoldValue temp in arrList) { |
| Console.WriteLine("Element{0} – Wert: {1}", i, temp.intVar); |
| i++; |
| } |
| } |
An der Konsole werden die Werte der Felder in der Reihenfolge 5, 17, 100 ausgegeben, obwohl die ursprüngliche Reihenfolge in der Liste eine andere war. Der Vergleich und die anschließende Sortierung finden also wie erwartet statt.
Der Code lässt sich aber auch noch eleganter formulieren. Wenn Sie sich in der .NET-Dokumentation die Definition der Struktur Int32 ansehen, werden Sie feststellen, dass dieser Typ seinerseits selbst die Schnittstelle IComparable implementiert. Es ist daher nahe liegend, den Vergleich am Feld intVar direkt vorzunehmen:
| public int CompareTo(object obj) { |
| HoldValue val = (HoldValue)obj; |
| return this.intVar.CompareTo(val.intVar); |
| } |
Damit können wir uns aber noch nicht ganz zufrieden geben, denn alle denkbaren Szenarien werden von unserer Implementierung noch nicht berücksichtigt. Es könnte nämlich auch ein Objekt übergeben werden, dass mit dem aktuellen nicht vergleichbar ist, beispielsweise:
| Circle kreis = new Circle(5); |
| HoldValue val = new HoldValue(8); |
| int x = val.CompareTo(kreis); |
Wenn Sie die Methode CompareTo implementieren, sollten Sie diesen Fall ebenso berücksichtigen wie die Eventualität, dass das übergebene Objekt noch nicht initialisiert und daher null ist. Die Implementierung, die diese beiden Szenarien einbezieht, sieht wie folgt aus:
| public int CompareTo(object obj) { |
| // prüfen, ob der Parameter ein null-Verweis ist |
| if(obj == null) |
| return 1; |
| // prüfen, ob beide Typen gleich sind |
| if(obj.GetType() != this.GetType()) |
| throw new ArgumentException("Ungültiger Vergleich"); |
| // Vergleich der beiden Objekte |
| HoldValue val = (HoldValue)obj; |
| return this.intVar.CompareTo(val.intVar); |
| } |
Generell sollten Sie die Methode CompareTo der Schnittstelle IComparable wie gezeigt implementieren, um gegen alle unzulässigen Aufrufe gewappnet zu sein. Es wird zuerst überprüft, ob dem Parameter null übergeben wurde. Der Vergleich sollte daraufhin abgebrochen werden und als Resultat einen Wert größer 0 liefern. Damit wird ein null-Verweis vor einem Objektverweis einsortiert. Unterscheiden sich die beiden Typen des anstehenden Vergleichs, wird die Ausnahme ArgumentException ausgelöst und muss vom Aufrufer behandelt werden.
Auf der Buch-CD finden Sie den Programmcode des Beispiels unter:
...\Kapitel 7\IComparableDemo
Das Sortieren einer ArrayList mit der parameterlosen Sort-Methode gestattet nur ein Vergleichskriterium. Manchmal ist aber erforderlich, unterschiedliche Sortierkriterien zu berücksichtigen. Nehmen wir zum Beispiel die Klasse Person, welche die beiden Felder Name und Wohnort beschreibt.
| class Person { |
| public string Name; |
| public string Wohnort; |
| public Person(string name, string ort) { |
| Name = name; |
| Wohnort = ort; |
| } |
| } |
Würden die Klasse die Schnittstelle IComparable implementieren, müsste die Entscheidung getroffen werden, nach welchem Feld Objekte dieser Klasse sortiert werden können. Nun sollen beide Möglichkeiten angeboten werden.
Die Lösung des Problems führt über die Bereitstellung so genannter Vergleichsklassen, welche die Schnittstelle IComparer implementieren. In jeder Vergleichsklasse wird genau ein Vergleichskriterium festgelegt. Wollen wir einen bestimmten Objektvergleich erzwingen, müssen wir der Sort-Methode mitteilen, welche Vergleichsklasse dafür bestimmt ist. Dafür stehen uns zwei Überladungen zur Verfügung, denen die Referenz auf ein Objekt übergeben wird, das die Schnittstelle IComparer implementiert:
| public virtual void Sort(IComparer); |
| public virtual void Sort(int, int, IComparer); |
Mit der Überladung, die zwei int erwartet, können der Startindex und die Länge des zu sortierenden Bereichs bestimmt werden. Bei sehr großen Auflistungen steigert das die Performance, da Sortiervorgänge sehr rechenintensiv sind.
Die Schnittstelle IComparer stellt eine Methode für den Vergleich zweier Objekte bereit:
| int Compare(object x, object y); |
Compare funktioniert ähnlich der weiter oben erörterten Methode CompareTo und gibt die folgenden Werte zurück:
| < 0, wenn das erste Objekt »kleiner« als das zweite Objekt ist. |
| 0, wenn das erste Objekt »gleich« dem zweiten Objekt ist. |
| > 0, wenn das erste Objekt »größer« als das zweite Objekt ist. |
| Hinweis Der große Unterschied zwischen den beiden Schnittstellenmethoden IComparable. CompareTo und IComparer.Compare ist die Parameterliste, und der daraus resultierende Methodenaufruf. CompareTo nimmt eine Referenz entgegen, die mit dem aktuellen Objekt verglichen wird, während Compare zwei zu vergleichende Referenzen übergeben werden. Somit ist diese Methode auch unabhängig von der this-Referenz. Die Schnittstelle IComparer bietet sich daher auch an, wenn Sie die Objekte eines Typs vergleichen wollen, der nicht IComparable implementiert. |
Für die Klasse Person wollen wir nun die beiden Vergleichsklassen NameComparer und WohnortComparer entwickeln, die gemäß Forderung die Schnittstelle IComparer implementieren und nach Wohnort bzw. Name sortieren.
| // Vergleichsklasse – Kriterium 'Wohnort' |
| class WohnortComparer : IComparer { |
| public int Compare(object x, object y) { |
| // prüfen auf null-Übergabe |
| if(x == null && y == null) return 0; |
| if(x == null) return 1; |
| if(y == null) return –1; |
| // Typüberprüfung |
| if(x.GetType() != y.GetType()) |
| throw new ArgumentException("Ungültiger Vergleich"); |
| // Vergleich |
| return ((Person)x).Wohnort.CompareTo(((Person)y).Wohnort); |
| } |
| } |
| // Vergleichsklasse – Kriterium 'Name' |
| class NameComparer : IComparer { |
| public int Compare(object x, object y) { |
| // prüfen auf null-Übergabe |
| if(x == null && y == null) return 0; |
| if(x == null) return 1; |
| if(y == null) return –1; |
| // Typüberprüfung |
| if(x.GetType() != y.GetType()) |
| throw new ArgumentException("Ungültiger Vergleich"); |
| // Vergleich |
| return ((Person)x).Name.CompareTo(((Person)y).Name); |
| } |
| } |
Die Implementierung ähnelt der der Methode CompareTo. Zuerst sollte wieder ein Vergleich mit null durchgeführt werden und anschließend eine Prüfung, ob beide Parameter denselben Typ beschreiben oder zumindest einen vergleichbaren Typ besitzen. Sollte keine Bedingung zutreffen, kann der Vergleich der Objekte erfolgen. Dabei unterstützt uns die Klasse String, die ihrerseits die IComparable-Schnittstelle implementiert, mit der Methode CompareTo.
Haben wir ein ArrayList-Objekt mit Person-Objekten gefüllt, steht es uns frei, welche Vergleichsklasse wir zur Sortierung der Objekte benutzen, denn beide sind auf dieselbe Schnittstelle zurückzuführen und gegenseitig austauschbar.
| // ----------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\IComparerDemo |
| // ----------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| ArrayList arrList = new ArrayList(); |
| // ArrayList füllen |
| Person pers1 = new Person("Meier", "Berlin"); |
| arrList.Add(pers1); |
| Person pers2 = new Person("Arnhold", "Köln"); |
| arrList.Add(pers2); |
| Person pers3 = new Person("Graubär", "Aachen"); |
| arrList.Add(pers3); |
| // nach Wohnorten sortieren |
| arrList.Sort(new WohnortComparer()); |
| Console.WriteLine("Liste nach Wohnorten sortiert"); |
| ShowSortedList(arrList); |
| // nach Namen sortieren |
| arrList.Sort(new NameComparer()); |
| Console.WriteLine("Liste nach Namen sortiert"); |
| ShowSortedList(arrList); |
| } |
| static void ShowSortedList(IList liste) { |
| foreach(Person temp in liste) { |
| Console.Write("Name = {0,-12}", temp.Name); |
| Console.WriteLine("Wohnort = {0}", temp.Wohnort); |
| } |
| Console.WriteLine(); |
| } |
| } |
Mit der statischen Methode Adapter kann ein Wrapper (darunter ist eine Klasse zu verstehen, die sich um eine andere legt) um ein IList-Objekt gelegt werden. Der Rückgabewert ist die Referenz auf ein neues ArrayList-Objekt, auf dessen Methoden sich das IList-Objekt manipulieren lässt.
| public static ArrayList Adapter(IList list); |
Wie Sie die Methode Adapter einsetzen können, möchte ich Ihnen an einem Beispiel zeigen. Wie Sie der .NET-Dokumentation zu IList entnehmen können, implementiert auch ein gewöhnliches Array diese Schnittstelle. Ein Array kann allerdings nicht sortiert werden. Über den Aufruf von Adapter wird das allerdings möglich.
Nehmen wir an, die Klasse Person sei wie folgt definiert:
| public class Person { |
| public string Name; |
| public int Alter; |
| public Person(string name, int alter) { |
| Name = name; |
| Alter = alter; |
| } |
| } |
Ein Array vom Typ Person soll mehrere Objekte enthalten, die dem Namen nach sortiert werden sollen. Dazu rufen wir die statische Methode Adapter unter Übergabe des Arrays auf und weisen die zurückgelieferte Referenz der Methode einer ArrayList-Variablen zu.
| Person[] pers = new Person[3]; |
| pers[0] = new Person("Peter", 15); |
| pers[1] = new Person("Alfred", 33); |
| pers[2] = new Person("Hugo", 26); |
| ArrayList liste = ArrayList.Adapter(pers); |
Die Elemente des Arrays sollen jetzt dem nach Namen sortiert werden. Dazu bietet sich eine Überladung der Methode Sort der ArrayList an:
| public virtual void Sort(IComparer); |
Da ein Array die Schnittstelle IComparer nicht implementiert, die notwendig wäre, um diese Überladung aufzurufen, müssen wir eine Vergleichsklasse, welche die Schnittstelle IComparer implementiert, bereitstellen:
| public class SortByName : IComparer { |
| public int Compare(object x, object y) { |
| return ((Person)x).Name.CompareTo(((Person)y).Name); |
| } |
| } |
Mit dem Aufruf von Sort unter Übergabe eines Objekts vom Typ der Vergleichsklasse SortByName können wir über den bereitgestellten Wrapper die Elemente des Arrays pers in die gewünschte Reihenfolge bringen:
| liste.Sort(new SortByName()); |
Den vollständigen Programmcode zu diesem Beispiel finden Sie auf der Buch-CD unter:
.\Kapitel 7\ArrayList_Adapter)
IList-Auflistungen verwalten die Objekten über Indizes. Dieses Konzept hat aber einen Nachteil: Wenn man nach einem bestimmten Element sucht und dessen Position nicht kennt, muss man die Liste so lange durchlaufen, bis man eine Übereinstimmung findet. Enthält die Auflistung sehr viele Einträge, kann das sehr zeitaufwändig sein und kostet Rechenleistung.
Kommt es nicht auf die Reihenfolge der Elemente an, kann man sich für eine Auflistung, die das Interface IDictionary implementiert, entscheiden. Dazu gehört die Klasse Hashtable, die unten vorgestellt wird. In diesen Auflistungen kann ein bestimmtes Element zwar schneller gefunden werden, allerdings muss man dabei in Kauf, keinen Einfluss auf die Positionierung der Elemente in der Liste zu haben. IDictionary-Collections organisieren die Elemente in einer für sie passenden Reihenfolge.
Um nach einem Element in einer IDictionary-Auflistung zu suchen, wird eine Schlüsselinformation benötigt, der ein Wert zugeordnet ist. IDictionary-Auflistungen enthalten Elemente mit Schlüssel-Wert-Kombinationen. Der Schlüssel muss eindeutig sein und darf nicht den Inhalt null haben. In einer IList-Collection entspricht der Schlüssel dem Index. Der wesentliche Unterschied ist dabei jedoch, dass der Schlüssel nicht garantiert, eindeutig einem bestimmten Eintrag zugeordnet zu sein.
Stellen Sie sich dazu vor, Sie beabsichtigten, die Mitarbeiter eines Unternehmens in einer Auflistung zu verwalten. Jeder Mitarbeiter ist über eine eindeutige Personalnummer identifizierbar. Diese Personalnummer beschreibt gleichzeitig, welche persönlichen Daten zu dem Mitarbeiter gehören:
| 0999–123–3 = Franz Fischer |
| 0100–288–3 = Peter Müller |
| 6771–771–1 = Marita Kohl |
Hinter jeder Personalnummer verbirgt sich genau ein Mitarbeiter, aber einem Mitarbeiter könnten durchaus auch zwei Personalnummern zugewiesen werden – vielleicht weil er zwei separat honorierte Positionen besetzt. Diese drei Wertpaare ließen sich problemlos durch eine IDictionary-Auflistung abbilden. Der Schlüssel würde durch die Personalnummer beschrieben, der Name entspräche dem Wert. In der Realität würde man dann allerdings wahrscheinlich sinnvollerweise den Namen durch eine Objektreferenz ersetzen, die auf das dem Mitarbeiter zugeordnete Objekt verweist. Der Schlüssel kann durchaus selbst ein Objekt sein, wird aber häufig durch Zeichenfolgen beschrieben.
Die meisten der von IDictionary veröffentlichten Methoden sind uns bereits aus der Schnittstelle IList bekannt. Das erleichtert zwar einerseits die Einarbeitung, zwingt uns aber andererseits dennoch in einigen Fällen zu einer etwas genaueren Betrachtung.
Jeder Listeneintrag in einer IDictionary-Auflistung wird durch ein Schlüssel-Wert-Paar beschrieben, was sich in der Parameterliste der Add-Methode niederschlägt:
| void Add(object key, object value); |
Der erste Parameter wird als Schlüssel für das hinzuzufügende Element verwendet und sorgt für die Identifizierbarkeit innerhalb einer Liste, der zweite ist die Referenz auf das hinzuzufügende Element. Wir stoßen hier zum ersten Mal auf die Tatsache, dass von IDictionary-Auflistungen anstelle eines Index ein Schlüssel verwendet wird.
Der Schlüssel begleitet uns durch alle Methoden und wird auch von Remove zum Entfernen eines Objekts aus der Auflistung verwendet:
| void Remove(object key); |
Da IDictionary-Objekte nicht über Indizes verwaltet werden, brauchen nach dem Löschen eines Elements etwaige Folgeelemente auch keine Lücke zu schließen.
Dem Indexer kommt nicht nur die Aufgabe zu, unter der Angabe des Schlüssels den Zugriff auf das gewünschte Element zu gewährleisten, vielmehr kann er auch dazu benutzt werden, den Wert eines Objekts zu verändern.
| object this[object key] {get; set;} |
Gibt man einen Schlüssel an, der sich noch nicht in der Auflistung befindet, wird das Element hinzugefügt. Dabei bleibt der Wert leer, ist also null, was durchaus zulässig ist.
Die Schlüssel und die Werte werden in eigenen Auflistungen verwaltet. Die Referenz auf diese internen Auflistungen liefern die Eigenschaften Keys und Values.
| ICollection Keys {get;} |
| ICollection Values {get;} |
Mit Clear kann eine IDictionary-Auflistung geleert werden, mit Contains können wir prüfen, ob ein bestimmter Schlüssel bereits in der Liste enthalten ist.
| Eigenschaft/Methode | Beschreibung |
| Add | Hinzufügen eines Objekts zur Auflistung. |
| Remove | Löschen eines Elements aus der Auflistung. |
| Item | Zugriff auf ein Element der Auflistung. |
| Keys | Liefert alle in der Liste verwendeten Schlüssel zurück. |
| Values | Liefert alle in der Liste verwendeten Werte zurück. |
| Clear | Löscht alle Elemente der Auflistung. |
| Contains | Prüft, ob ein bestimmter Schlüssel in der Auflistung enthalten ist. |
Die wichtigste Auflistung, die das IDictionary-Interface implementiert, wird von der Klasse Hashtable beschrieben. Dieser Auflistungstyp ist eine Datenstruktur, die ein schnelles Suchen nach Objekten erlaubt. Der Name rührt daher, dass für die Verwaltung der Elemente ein Hashwert für den Schlüssel verwendet wird. Zum Erzeugen des Hashwerts wird intern die von Object geerbte Methode GetHashCode ausgeführt.
Im folgenden Beispiel wird eine Hashtabelle erzeugt, die vier Objekte vom Typ ClassA sowie eine Zeichenfolge verwaltet. Damit wir sehen, wie wir über den Tabelleneintrag auf das Member eines registrierten Elements zugreifen, ist in der Definition der ClassA die öffentliche Eigenschaft intVar deklariert, der wir über den Konstruktor einen Wert übergeben. Im Beispielcode werden die wichtigsten Methoden einer Hashtabelle benutzt, um Informationen sowohl über die Elemente als auch über die Listeneinträge zu erhalten.
| // ------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\Hashtabelle |
| // ------------------------------------------------------------- |
| class Program { |
| static ClassA obj1 = new ClassA(1); |
| static ClassA obj2 = new ClassA(2); |
| static ClassA obj3 = new ClassA(3); |
| static ClassA obj4 = new ClassA(4); |
| static Hashtable myHash; |
| static void Main(string[] args) { |
| myHash = new Hashtable(); |
| // Objekte der Hashtabelle hinzufügen |
| AddObjects(); |
| // Liste der Schlüssel ausgeben |
| GetKeyList(); |
| // Liste der Werte ausgeben |
| GetValueList(); |
| // Liste der Schlüssel und Werte ausgeben |
| GetCompleteList(); |
| Console.WriteLine(); |
| // Zugriff auf ein bestimmtes Element |
| Console.Write("Geben Sie nun den Schlüssel "); |
| Console.Write("des Objekts ein, dessen Eigenschaft "); |
| Console.Write("intVar Sie auswerten wollen: "); |
| string input = Console.ReadLine(); |
| // prüfen, ob der Schlüssel sich in der Hashtabelle befindet |
| if(myHash.Contains(input)) { |
| Console.Write("Das Objekt {0} ", input); |
| Console.Write("hat in intVar den Inhalt {0}", |
| (ClassA)myHash[input]).intVar); |
| Console.WriteLine(); |
| } |
| else { |
| Console.WriteLine("Nicht Element der Hashtabelle"); |
| } |
| // anhand des Wertes prüfen, ob sich ein Objekt |
| // bereits in der Hashtabelle befindet |
| Console.Write("Aufruf von ContainsValue: "); |
| if(myHash.ContainsValue(obj2)) |
| Console.WriteLine("Das Objekt ist enthalten."); |
| else |
| Console.WriteLine("Das Objekt ist enthalten."); |
| Console.ReadLine(); |
| } |
| // Ausgabe der Werteliste |
| public static void GetValueList() { |
| Console.WriteLine(); |
| Console.WriteLine("===== Werteliste ====="); |
| foreach(object obj in myHash.Values) |
| Console.WriteLine(obj); |
| } |
| // Ausgabe der Schlüsselliste |
| public static void GetKeyList() { |
| Console.WriteLine(); |
| Console.WriteLine("===== Schlüsselliste ====="); |
| foreach(object obj in myHash.Keys) |
| Console.WriteLine(obj); |
| } |
| // Schlüssel-Wert-Paar über ein DictionaryEntry-Objekt ausgeben |
| public static void GetCompleteList() { |
| Console.WriteLine(); |
| Console.WriteLine("===== Schlüssel-/Wertepaare ====="); |
| foreach(DictionaryEntry dicEntry in myHash) { |
| Console.Write(dicEntry.Key); |
| Console.WriteLine(" – {0}", dicEntry.Value); |
| } |
| } |
| // Objekte der Hashtabelle hinzufügen |
| public static void AddObjects(){ |
| myHash.Add("eins", obj1); |
| myHash.Add("zwei", obj2); |
| myHash.Add("drei", obj3); |
| myHash.Add("vier", obj4); |
| myHash.Add("fünf", "Hallo"); |
| } |
| } |
| class ClassA { |
| public int intVar; |
| public ClassA(int x) { |
| intVar = x;} |
| } |
Das Objekt myHash vom Typ Hashtable wird mit dem parameterlosen Konstruktor erzeugt, der meistens ausreichen dürfte. Anschließend werden in der benutzerdefinierten Methode AddObjects fünf ClassA-Objekte in der Hashtabelle registriert. Dem Aufruf der Methode Add werden dazu Schlüssel und Wert übergeben. Im Beispiel ist der Schlüssel eine Zeichenfolge, die Objektreferenz stellt den Wert dar. Ist ein Schlüssel bereits in der Hashtabelle enthalten, kommt es zur Auslösung der Ausnahme ArgumentException.
Der Inhalt einer HashTable lässt sich abfragen – sowohl die Liste der Schlüssel als auch die Liste der Werte. Dazu dienen die Eigenschaften Keys und Values. In den Methoden GetKeyList und GetValueList wird in jeweils einer foreach-Schleife die Werte- bzw. Schlüsselliste durchlaufen. Beachten Sie, dass die Laufvariablen der Schleifen nicht dazu benutzt werden können, auf das Listenelement zuzugreifen, um in unserem Fall beispielsweise das Feld intVar auszuwerten. Daher wird auch zur Laufzeit eine Ausnahme ausgelöst, wenn Sie versuchen, die Laufvariable mit
| // Vorsicht: Falsche Konvertierung |
| foreach(object obj in myHash) |
| Console.WriteLine(((ClassA)obj).intVar); |
oder mit
| // Vorsicht: Falsche Konvertierung |
| foreach(object obj in myHash) |
| Console.WriteLine(((ClassA)myHash[obj]).intVar); |
zu konvertieren.
Um auf ein Listenelement in einer foreach-Schleife zugreifen zu können, können Sie die Laufvariable vom Typ DictionaryEntry deklarieren. Tatsächlich sind die Elemente in einer HashTable von diesem Typ. DictionaryEntry ist eine Struktur, die das Schlüssel-Wert-Paar für einen Hashtabelleneintrag enthält. Über die Eigenschaften Key und Value können wir die notwendigen Informationen beziehen. Während uns Key nur den Schlüssel liefert, können wir über den Rückgabewert von Value nach vorheriger Typumwandlung auf das Objekt zugreifen:
| foreach(DictionaryEntry dicEntry in myHash) { |
| ... |
| Console.WriteLine(((ClassA)dicEntry.Value).intVar); |
| } |
| Hinweis Dass die Einträge in einer Hashtabelle vom Typ DictionaryEntry sind, müssen Sie berücksichtigen, wenn Sie mit der Methode CopyTo die Einträge in ein Array kopieren wollen. Das Array muss dann vom diesem Typ oder vom Typ object sein. |
Eine HashTable dient zur Verwaltung mehrerer meist gleichartiger Objekte und hat im Vergleich zu anderen Auflistungen den Vorteil, einen sehr schnellen Zugriff über den Indexer zu ermöglichen. Im Beispiel oben wird der Benutzer an der Konsole dazu aufgefordert, einen Schlüssel anzugeben, nach dem in der Hashtabelle gesucht werden soll. Ob der Schlüssel einem Element der Auflistung zugeordnet werden kann, wird durch die Methode Contains festgestellt, die einen booleschen Wert zurückliefert:
| if(myHash.Contains(input))... |
Analog könnte man auch die Methode ContainsKey benutzen, die sich in keiner Weise von Contains unterscheidet.
Nicht nur über den Schlüssel lässt sich prüfen, ob ein Element Mitglied der Hashtabelle ist. Auch über den booleschen Rückgabewert von ContainsValue ist das möglich. Im Beispiel wird dazu direkt die Referenz obj2 übergeben, die natürlich immer zu derselben Konsolenausgabe führt:
| if(myHash.ContainsValue(obj2))... |
Weder die Klasse Queue noch die Klasse Stack implementiert das Interface IList oder IDictionary. Dennoch werden beide den Auflistungen zugerechnet, weil sie die Schnittstellen ICollection und somit auch IEnumerable implementieren.
Stack ist eine Datenstruktur, die nach dem LIFO-Prinzip (Last-In-First Out) arbeitet: Das Element, das als letztes eingefügt wurde, wird beim folgenden Lesevorgang wieder entnommen. Daraus folgt, dass man auf das Element, das als erstes auf den Stack gelegt worden ist, erst dann wieder zugreifen kann, wenn alle anderen Elemente den Stack verlassen haben.
Ein Queue-Objekt ist das Pendant zu Stack. Es arbeitet nach dem FIFO-Prinzip (First-In-First Out), das besagt, dass das zuerst in die Queue geschobene Element auch als erstes wieder entnommen wird. Das Prinzip gleicht also einer Warteschlange an der Kasse eines Fußballstadions.
Schauen wir uns an einem Beispiel an, wie man mit der Klasse Stack arbeitet.
| // ------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\StackClass |
| // ------------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| Stack myStack = new Stack(11); |
| // Stack füllen |
| for(int i = 0; i <= 10; i++) |
| myStack.Push(i * i); |
| // Ausgabe an der Konsole |
| PrintStack(myStack); |
| Console.ReadLine(); |
| } |
| public static void PrintStack(Stack obj) { |
| // alle Elemente aus dem Stack holen |
| while(obj.Count != 0) { |
| Console.WriteLine(obj.Pop()); |
| } |
| } |
| } |
Das Hinzufügen neuer Elemente geschieht durch den Aufruf der Methode Push, die als Argument ein Objekt erwartet. Im Beispielcode wird eine Schleife durchlaufen, in der insgesamt elf Zahlen auf den Stack gelegt werden. Es handelt sich dabei immer um das Quadrat des aktuellen Schleifenzählers.
Zugegriffen werden kann nur auf das oberste Element im Stack. Dabei handelt es sich immer um das Objekt, das als letztes mit der Push-Methode auf den Stack gelegt wurde.
Es bieten sich zwei Alternativen an, das oberste Element auszuwerten: Mit Pop wird das oberste Element nicht nur zurückgeliefert, sondern gleichzeitig auch der Stack-Verwaltung entzogen. Mit Peek erhält man zwar die Referenz, ohne es jedoch gleichzeitig zu entfernen. Im Beispiel wird der Stack so lange mit Pop abgegriffen, bis die Liste wieder leer ist. Die Reihenfolge der Zahlen beim Hinzufügen lautete:
| 0 1 4 9 16 25 36 ... 81 100 |
Die Rückgabe erfolgt mit:
| 100 81 64 ... 25 16 9 4 1 0 |
Der Aufruf des parameterlosen Konstruktors der Klasse Stack führt zu einer Standardkapazität von 32 Elementen, die bei Bedarf automatisch erhöht wird, um weitere Elemente aufzunehmen. Dabei werden alle Elemente in ein neues Array kopiert. Wenn Sie wissen, dass Sie diese Anzahl überschreiten werden, sollten Sie aus Gründen einer besseren Performance den parametrisierten Konstruktor wählen, der die Übergabe der erforderlichen Startkapazität ermöglicht:
| Stack stack = new Stack(100); |
Reicht das immer noch nicht aus und wird zur Laufzeit die Initialisierungsgröße trotzdem überschritten, verdoppelt sich die Kapazität automatisch.
Das Beispiel, das vorhin die Klasse Stack veranschaulichte, wird nun auf ein Queue-Objekt umgeschrieben:
| // ------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 7\QueueClass |
| // ------------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| Queue myQueue = new Queue(); |
| // Queue füllen |
| for(int i = 0; i <= 10; i++) |
| myQueue.Enqueue(i * i); |
| // Ausgabe an der Konsole |
| PrintStack(myQueue); |
| Console.ReadLine(); |
| } |
| public static void PrintStack(Queue obj) { |
| // alle Elemente aus dem Stack holen |
| while(obj.Count != 0) { |
| Console.WriteLine(obj.Dequeue()); |
| } |
| } |
| } |
Diesmal sind es die beiden Methoden Enqueue und Dequeue, mit denen Elemente in die Liste geschoben und wieder aus ihr geholt werden. Dequeue liefert nicht nur die Referenz des sich am Anfang befindlichen Elements, es holt dieses Element auch aus der Warteschlange. Wie bei der Klasse Stack können Sie sich mit Peek auch die Referenz dieses Elements besorgen und es gleichzeitig in der Liste lassen.
Die Elementzugriff erfolgt in derselben Reihenfolge, in der die Objekte der Liste hinzugefügt wurden: Das erste hinzugefügte Element wird auch als erstes herausgeholt, danach kann man das zweite in die Warteschlange gelegte holen usw. Ein Zugriff auf ein beliebiges Element ist weder beim Stack noch bei der Queue möglich.
Die Standardkapazität eines Queue-Objekts beträgt 32 Elemente, die Sie mittels eines anderen Konstruktors bei der Instanziierung bedarfsgerecht festlegen können.
Mit ArrayList, Hashtable, Queue und Stack haben Sie bereits die wichtigsten Auflistungsklassen kennen gelernt. Die .NET-Klassenbibliothek stellt darüber hinaus noch weitere, auf spezifische Anwendungsfälle optimierte Auflistungen bereit, von denen die meisten im Namespace System.Collections.Specialized zu finden sind.
| Hinweis Genau genommen unterschlage ich Ihnen an dieser Stelle eine ganz neue Gruppe von Auflistungen, die seit dem .NET Framework 2.0 verfügbar sind. Denn die Auflistungen unterteilen sich in zwei Gruppen: untypisierte Auflistungen typisierte Auflistungen (generische Auflistungen) |
| Generische Auflistungen wurden mit .NET Framework 2.0 eingeführt und bieten gegenüber den untypisierten den Vorteil, dass sie bereits zur Entwicklungszeit auf einen bestimmten Typ geprägt werden können, d. h., es wird ein ganz bestimmter Typ verwaltet. Generische Auflistungen finden Sie im Namespace System.Collections.Generic. Mit generischen Typen befassen wir uns im nächsten Abschnitt. |
In der folgenden Tabelle erhalten Sie einen Überblick über die Auflistungsklassen, mit denen wir uns nicht näher beschäftigt haben. Da wir uns bereits einige typische Auflistungen genauer angesehen haben, ist es sicherlich nicht mehr schwierig, sich im Bedarfsfall in die Fähigkeiten einer anderen einzuarbeiten. Letztendlich finden wir immer die gleichen Eigenschaften und Methoden vor, die sich meist nur in der Parameterliste unterscheiden.
| Klasse | Beschreibung |
| BitArray | Verwaltet einen Array von Bits. |
| CollectionsUtil | Eine Auflistung, bei der keine Unterscheidung zwischen Groß- und Kleinschreibung erfolgt. |
| HybridDictionary | Das Verhalten orientiert sich an der Anzahl der Listenelemente. Ist die Anzahl der Elemente gering, operiert diese Klasse als ListDictionary-Collection, wird die Anzahl größer, als Hashtable. |
| ListDictionary | Solange die Anzahl der Elemente kleiner zehn ist, werden die Operationen mit den Elementen schneller ausgeführt als bei einer Hashtable. |
| NameValueCollection | Verwaltet ein Schlüssel-Wert-Paar, wobei sowohl der Schlüssel als auch der Wert durch Zeichenfolgen beschrieben werden. Einem Schlüssel können mehrere Zeichenfolgen zugeordnet werden, d. h., der Schlüssel ist nicht eindeutig. |
| SortedList | Diese Auflistung verwaltet Schlüssel-Wert-Paare, die nach den Schlüsseln sortiert sind und auf die sowohl über Schlüssel als auch über Indizes zugegriffen werden kann. Damit vereint sie die Merkmale von Hashtable und ArrayList. |
| StringCollection | Eine Auflistung, die nur Zeichenfolgen enthält |
| StringDictionary | Ähnlich einer Hashtable, der Schlüssel ist jedoch immer eine Zeichenfolge |
Im Einzelfall kann es sich als schwierig erweisen, aus der großen Anzahl der angebotenen Typen die für die aktuellen Anforderungen am besten geeignete zu wählen. Im Ausschlussverfahren sollten Sie sich dem Typ nähern, der Ihnen unter den gegebenen Umständen die maximale Performance und das gewünschte Verhalten bietet.
Wissen Sie, dass der wahlfreie, also beliebige Zugriff auf die Listenelemente erforderlich ist, verabschieden sich bereits die ersten beiden Klassen aus dem Angebot (Stack und Queue). Das nächste Kriterium auf dem Weg zur Entscheidungsfindung dürfte die Antwort auf die Frage sein, ob die Verwaltung über einen Index gewünscht oder sogar gefordert wird. Das könnte beispielsweise der Fall sein, wenn in einer Schleife über einen Schleifenzähler die Listenelemente der Reihe nach besucht werden müssen. Die Wahl würde in diesem Fall ArrayList oder SortedList lauten.
Objekte, die sich durch eine Schlüssel-Wert-Kombination beschreiben lassen, werden meist in Auflistungen verwaltet, die nicht indexbasiert sind. Ist die Anzahl der Elemente sehr klein, würde sich der Typ ListDictionary anbieten, ist die Anzahl größer, eignet sich besser Hashtable. Falls Sie keinen Mut zur Entscheidung haben – mit HybridDictionary geben Sie die Verantwortung ab. Liegt eine Schlüssel-Wert-Kombination vor und können Sie dennoch nicht auf die Elementsortierung in der Liste verzichten, lautet die Entscheidung wieder SortedList.
In speziellen Sonderfällen wird man auch noch einen Blick auf andere Typen werfen müssen, aber mit den eben erwähnten sind sicherlich 95 % aller Anwendungsfälle abzudecken.
Sie suchen eine Auflistungsklasse, die ausschließlich bestimmte Typen verwaltet? Möglicherweise sogar Objekte eines benutzerdefinierten Typs? Sie werden mit Sicherheit keine passende Klasse mit der geforderten strikten Typbindung im .NET Framework finden. Sie haben jetzt zwei Alternativen:
1. Sie leiten eine passende, vom .NET Framework zur Verfügung gestellte Klasse ab, z.B. CollectionBase.| 2. | Sie entwickeln eine generische Auflistungsklasse oder benutzen eine aus der .NET-Klassenbibliothek. |
Ich werden Ihnen zuerst zeigen, wie Sie eine eigene Auflistungsklasse durch Ableitung bereitstellen. Im Abschnitt 7.4 werden wir uns mit den Generics auseinander setzen, die einen anderen, sicherlich auch einfacheren Weg aufzeigen. Nichtsdestotrotz halte ich es für sehr lehrreich, sich den Weg über die Ableitung anzusehen. Lassen Sie uns deshalb damit starten.
Angenommen wir hätten eine Klasse namens HoldValue entwickelt und wollen viele Objekte dieses Typs von einer Auflistung verwalten lassen. Ein erster Ansatz könnte sein, eine neue Klasse bereitzustellen, die ein Objekt vom Typ ArrayList aggregiert. Von unserer Auflistungsklasse werden Methoden und Eigenschaften veröffentlicht, die es ermöglichen, durch Weiterleitung die Methoden und Eigenschaften des internen ArrayList-Objekts zu bedienen.
| class HoldValue { |
| public int Value; |
| public HoldValue(int value) { |
| Value = value; |
| } |
| } |
| // benutzerdefinierte Klasse, die wie eine Auflistung agiert |
| class HoldValueList { |
| // Private Instanz der Klasse ArrayList |
| private ArrayList col; |
| // Konstruktor |
| public HoldValueList() { |
| col = new ArrayList(); |
| } |
| // Hinzufügen eines HoldValue-Objekts |
| public void Append(HoldValue obj) { |
| col.Add(obj); |
| } |
| // Löschen eines verwalteten HoldValue-Objekts |
| public void Delete(int index) { |
| col.RemoveAt(index); |
| } |
| ... |
| } |
Einem Umstand hatten wir dabei keine Aufmerksamkeit geschenkt: Auflistungen sind auch dadurch charakterisiert, dass auf ihre Elemente in einer foreach-Schleife der Reihe nach zugegriffen werden kann. Wie wir inzwischen wissen, setzt das voraus, dass die Auflistungsklasse die Schnittstelle IEnumerable mit ihrer Methode GetEnumerator implementiert. Davon ist HoldValueList aber weit entfernt.
Den Enumerator zu implementieren ist verhältnismäßig komplex. Um uns die Programmierung einfacher zu machen, stellt uns die .NET-Klassenbibliothek mit CollectionBase, ReadOnlyCollectionBase und DictionaryBase drei abstrakte Klassendefinitionen zur Verfügung, die wir für unsere Zwecke optimal nutzen können.
Am Beispiel von CollectionBase wollen nun die Klasse HoldValueList so implementieren, dass sie alle an eine Collection gestellten Anforderungen erfüllen kann. Sehen wir uns aber zuerst die Definition der Klasse an:
| public abstract class CollectionBase : IList, ICollection, IEnumerable |
CollectionBase implementiert alle erforderlichen Schnittstellen und ist abstrakt definiert, muss also abgeleitet werden. Die Schnittstelle IList deutet bereits an, dass sich ein Objekt vom Typ CollectionBase ähnlich wie eine ArrayList verhalten wird, die Elemente also indexbasiert verwaltet.
Werfen wir nun einen Blick auf die Mitglieder der Klasse in der Online-Dokumentation. Im ersten Moment mag die Länge der Liste schockieren. Wenn Sie sich etwas genauer damit beschäftigen, werden Sie erkennen, wie trick- und folglich auch für uns lehrreich die Implementierung ist und wie weit sie in die Tiefen der objektorientierten Programmierung eindringt.
Fangen wir mit dem Block der »Geschützten Eigenschaften« an. Hier können wir erkennen, dass auch CollectionBase das Rad nicht neu erfindet, sondern ein ArrayList-Objekt aggregiert, auf das die Eigenschaften InnerList die Referenz liefert:
| protected ArrayList InnerList {get;} |
Es gibt mit List noch eine zweite geschützte Eigenschaft. Diese liefert die Referenz auf die IList-Schnittstelle der CollecionBase.
| protected IList List {get;} |
Den Unterschied zwischen diesen beiden Referenzen und die daraus resultierenden Konsequenzen werde ich später erklären.
Unter »Öffentliche Eigenschaften« und »Öffentliche Methoden« finden wir neben den von object geerbten Membern auch Count, Clear und RemoveAt wieder. Diese sind unabhängig vom verwalteten Typ und bedürfen daher keiner besonderen Aufmerksamkeit.
Die Klasse CollectionBase ist bereits vollständig implementiert. Die Kennzeichnung mit abstract zwingt nur dazu, die Klasse abzuleiten, um sie zu typisieren. Alle Methoden, die von den Schnittstellen übernommen werden, funktionieren jedoch bereits tadellos.
CollectionBase implementiert die Schnittstelle IList und muss daher eine Add-Methode haben. Diese wird jedoch nicht veröffentlicht, sondern explizit implementiert, was einer »Privatisierung« gleichkommt. Damit hat man über eine Referenz der Klasse HoldValueList keinen direkten Zugriff auf die Add-Methode, sondern nur über eine IList-Referenz:
| HoldValueList liste = new HoldValueList(); |
| IList ilist = (IList)liste; |
| ilist.Add(new HoldValue(3)); |
| Console.WriteLine(liste.Count); |
Tatsächlich wird dieser Code an der Konsole die Zahl 1 ausgeben, das Objekt wurde also hinzugefügt.
Bis auf Clear, RemoveAt und Count implementiert CollectionBase alle Methoden von IList und ICollection explizit (siehe dazu auch im Block »Explizite Schnittstellenimplementierung« in der .NET-Dokumentation). Auch wenn wir jetzt wissen, wie wir unsere Klasse HoldValueList austricksen können, die Forderung, nur HoldValue-Objekte zu verwalten, erfüllt sie noch nicht.
Exemplarisch für alle anderen Methoden wollen wir daher jetzt die Add-Methode in HoldValueList implementieren, damit auf eine HoldValueList-Referenz die übliche Methode zum Hinzufügen eines Eintrags aufgerufen werden kann. Der Parameter wird nun spezialisiert, er ist vom Typ HoldValue.
| class HoldValueList : CollectionBase { |
| public void Add(HoldValue obj) { |
| this.InnerList.Add(obj); |
| } |
| } |
Die übergebene Referenz wird in die aggregierte Auflistung, deren Referenz InnerList liefert, eingetragen. Beachten Sie, dass hier nicht List verwendet wird. Damit ist sichergestellt, dass wir der benutzerdefinierten Auflistung HoldValueList nur einen bestimmten Typ übergeben können – oder etwa nicht? Nein, denn weiterhin kann die Add-Methode mit
| IList ilist = (IList)liste; |
| ilist.Add(new ClassA(3)); |